﻿using System;
using System.Linq.Expressions;
using System.Reflection;

using Cookbook.Common;

namespace Cooking.Services.Specs.Support
{
    public static class ReflectionExtensions
    {
        /// <summary>
        /// Gets the (possibly private or protected) field of an object using reflection.
        /// </summary>
        public static T GetField<T>(this object obj, string fieldName)
        {
            var type = obj.GetType();
            var field = GetFieldInfoRecursively(type, type.Name, fieldName);
            return (T)field.GetValue(obj);
        }

        /// <summary>
        /// Gets the (possibly private or protected) static field of a class using reflection.
        /// </summary>
        public static TField GetStaticField<TClass, TField>(string fieldName)
        {
            var type = typeof(TClass);
            var field = GetFieldInfoRecursively(type, type.Name, fieldName);
            return (TField)field.GetValue(null);
        }

        /// <summary>
        /// Sets the (possibly private or protected) field of an object using reflection.
        /// </summary>
        public static void SetCamelCaseField<T, TProperty>(this T obj, Expression<Func<T, TProperty>> propertyExpression, object value)
        {
            PropertyInfo pi = propertyExpression.GetPropertyInfo();
            string fieldName = ToCamelCase(pi);
            obj.SetField(fieldName, value);
        }

        private static string ToCamelCase(PropertyInfo pi)
        {
            return pi.Name.Substring(0, 1).ToLower() + pi.Name.Substring(1);
        }

        /// <summary>
        /// Sets the (possibly private or protected) field of an object using reflection.
        /// </summary>
        public static void SetField(this object obj, string fieldName, object value)
        {
            var type = obj.GetType();
            var field = GetFieldInfoRecursively(type, type.Name, fieldName);

            field.SetValue(obj, value);
        }

        private static FieldInfo GetFieldInfoRecursively(Type typeToCheckRecursively, string typeName, string fieldName)
        {
            var field = typeToCheckRecursively.GetField(fieldName,
               BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);

            if (field == null)
            {
                if (typeToCheckRecursively.BaseType != null)
                {
                    return GetFieldInfoRecursively(typeToCheckRecursively.BaseType, typeName, fieldName);
                }
                throw new ArgumentException("Field " + fieldName + " does not exist on " + typeName);
            }
            return field;
        }

        /// <summary>
        /// Gets the (possibly private or protected) property of an object using reflection.
        /// </summary>
        public static T GetProperty<T>(this object obj, string propertyName)
        {
            var type = obj.GetType();
            var property = GetPropertyInfoRecursively(type, type.Name, propertyName);
            return (T)property.GetValue(obj, null);
        }

        /// <summary>
        /// Sets the (possibly private or protected) property of an object using reflection.
        /// </summary>
        public static void SetProperty<T, TProperty>(this T obj, Expression<Func<T, TProperty>> propertyExpression, TProperty value)
        {
            PropertyInfo pi = propertyExpression.GetPropertyInfo<T, TProperty>();
            SetProperty(obj, pi.Name, value);
        }

        /// <summary>
        /// Sets the (possibly private or protected) property of an object using reflection.
        /// </summary>
        private static void SetProperty(this object obj, string propertyName, object value)
        {
            var type = obj.GetType();
            var property = GetPropertyInfoRecursively(type, type.Name, propertyName);

            if (property.DeclaringType != type)
            {
                property = property.DeclaringType.GetProperty(propertyName,
                   BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
            }

            property.SetValue(obj, value, null);
        }

        private static PropertyInfo GetPropertyInfoRecursively(Type typeToCheckRecursively, string typeName, string propertyName)
        {
            var property = typeToCheckRecursively.GetProperty(propertyName,
               BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);

            if (property == null)
            {
                if (typeToCheckRecursively.BaseType != null)
                {
                    return GetPropertyInfoRecursively(typeToCheckRecursively.BaseType, typeName, propertyName);
                }
                throw new ArgumentException("Property " + propertyName + " does not exist on " + typeName);
            }
            return property;
        }

        public static object InvokeMethod(this object obj, string methodName, object[] args)
        {
            Type type = obj.GetType();

            return type.InvokeMember(methodName,
               BindingFlags.InvokeMethod |
               BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null,
               obj, args);
        }
    }
}